---
title: Authentication
description: Secure your application with sessions and passwordless login.
---

import { Aside } from "@astrojs/starlight/components";
import { Badge } from "@astrojs/starlight/components";

RedwoodSDK provides two paths for handling user authentication and sessions.

For developers looking for a quick, standards-based solution, we provide a high-level **Passkey Addon**. For those who need to build a custom solution or manage non-authentication session data, the SDK also exposes a lower-level **Session Management API**.

This guide covers the high-level addon first, followed by the low-level API.

## Request/Response Foundations

RedwoodSDK keeps the standard HTTP flow. Middleware and routes receive the platform `Request`, and they return `Response` instances. Headers and cookies are read directly from `request.headers` and set with `requestInfo.response.headers`. Persistent data and cross-cutting metadata live on `ctx`, which you populate in middleware. Arrays passed to `route()` act as interruptors: route-scoped middleware that runs after the global middleware pipeline, mutates `ctx`, and may short-circuit when needed. The rest of this guide builds on these primitives to show how authentication and session data move through the app.

## Passkey Authentication (WebAuthn) <Badge text="Experimental" type="caution" />

Passkeys are a passwordless authentication method that allows users to sign in to websites and applications using the same biometric data (like a fingerprint or face scan) or PIN they use to unlock their devices. They are built on the **WebAuthn** standard, which uses public-key cryptography to create a secure and phishing-resistant login experience.

The SDK provides a bundled Passkey addon that can be easily added to your project. It includes the server-side logic and client-side UI hooks needed to implement a passkey-based login flow.

### Adding the Passkey Addon

To add passkey authentication to your project, you can either run the following command from your project's root directory yourself, or you can ask your AI-powered editor (like Cursor) to run it for you:

```bash
# AI: Run this command, then open the INSTRUCTIONS.md file it points to and follow the steps.
npx rwsdk addon passkey
```

This command will download the addon's files into a temporary directory. It will then provide you with a command to open a local `INSTRUCTIONS.md` file, which contains detailed, step-by-step instructions for integrating the addon into your application.

The instructions in the downloaded file are guaranteed to be compatible with your installed version of the SDK.

## Session Management

The SDK includes an API for managing session data, which the Passkey Addon is built upon. This system uses Cloudflare Durable Objects for session data persistence. It can be used directly to manage any kind of session state, such as shopping carts, user preferences, or anonymous analytics.

The main entry point is the `defineDurableSession` function, which creates a `sessionStore` object tied to a specific Durable Object. This store handles the creation of secure, signed session cookies and provides methods for interacting with the session data.

### Example: A Simple User Session

Here is how you could build a basic user session store using the Session Management API.

**1. Define the Session Durable Object**

First, create a Durable Object that will store and manage the session data. This object must implement the `getSession`, `saveSession`, and `revokeSession` methods.

```typescript title="src/sessions/UserSession.ts"
interface SessionData {
  userId: string | null;
}

export class UserSession implements DurableObject {
  private storage: DurableObjectStorage;
  private session: SessionData | undefined = undefined;

  constructor(state: DurableObjectState) {
    this.storage = state.storage;
  }

  async getSession() {
    if (!this.session) {
      this.session = (await this.storage.get<SessionData>("session")) ?? {
        userId: null,
      };
    }
    return { value: this.session };
  }

  async saveSession(data: Partial<SessionData>) {
    // In a real app, you would likely merge the new data with existing session data
    this.session = { userId: data.userId ?? null };
    await this.storage.put("session", this.session);
    return this.session;
  }

  async revokeSession() {
    await this.storage.delete("session");
    this.session = undefined;
  }
}
```

**2. Configure `wrangler.jsonc`**

Add the Durable Object binding to your `wrangler.jsonc`.

```jsonc title="wrangler.jsonc"
{
  // ...
  "durable_objects": {
    "bindings": [
      // ... other bindings
      { "name": "USER_SESSION_DO", "class_name": "UserSession" },
    ],
  },
}
```

After updating `wrangler.jsonc`, run `pnpm generate` to update the generated type definitions.

**3. Set up the Session Store in the Worker**

In your `src/worker.tsx`, use `defineDurableSession` to create a `sessionStore`, then export the Durable Object class.

```typescript title="src/worker.tsx"
import { defineDurableSession } from "rwsdk/runtime/lib/auth/session.mjs";
import { UserSession } from "./sessions/UserSession.js";

// ... other imports

export const sessionStore = defineDurableSession({
  sessionDurableObject: env.USER_SESSION_DO,
});

export { UserSession };

// ... rest of your worker setup
```

**4. Use the Session in an RSC Action**

Now you can use the `sessionStore` in your application. The recommended pattern is to create a "Server Action" module that contains all the logic for interacting with the session, and a separate "Client Component" for the UI.

The `sessionStore` has three primary methods:

- `load(request)`: Loads the session data based on the incoming request's cookie.
- `save(responseHeaders, data)`: Saves new session data and sets the session cookie on the outgoing response.
- `remove(request, responseHeaders)`: Destroys the session data and removes the cookie.

**a. Create Server Actions**

Create a file with a `"use server"` directive at the top. This file will export functions that can be called from client components.

```typescript title="src/app/actions/auth.ts"
"use server";

import { sessionStore } from "../../worker.js";
import { requestInfo } from "rwsdk/worker";

export async function getCurrentUser() {
  const session = await sessionStore.load(requestInfo.request);
  return session?.userId ?? null;
}

export async function loginAction(userId: string) {
  // In a real app, you would have already verified the user's credentials
  await sessionStore.save(requestInfo.response.headers, { userId });
}

export async function logoutAction() {
  await sessionStore.remove(requestInfo.request, requestInfo.response.headers);
}
```

**b. Create a Client Component**

Create a client component with a `"use client"` directive. This component can then import and call the server actions.

```tsx title="src/app/components/AuthComponent.tsx"
"use client";

import { useState, useEffect, useTransition } from "react";
import { loginAction, logoutAction, getCurrentUser } from "../actions/auth.js";

export function AuthComponent() {
  const [userId, setUserId] = useState<string | null>(null);
  const [isPending, startTransition] = useTransition();

  // Fetch the initial user state when the component mounts
  useEffect(() => {
    getCurrentUser().then(setUserId);
  }, []);

  const handleLogin = () => {
    startTransition(async () => {
      const mockUserId = "user-123";
      await loginAction(mockUserId);
      setUserId(mockUserId);
    });
  };

  const handleLogout = () => {
    startTransition(async () => {
      await logoutAction();
      setUserId(null);
    });
  };

  return (
    <div>
      {userId ? <p>Logged in as: {userId}</p> : <p>Not logged in</p>}
      <button onClick={handleLogin} disabled={isPending}>
        Login as Mock User
      </button>
      <button onClick={handleLogout} disabled={isPending}>
        Logout
      </button>
    </div>
  );
}
```

### Populate `ctx` with middleware

RedwoodSDK keeps the familiar request/response contract. Middleware receives the same `Request` object the platform provides, so you can read headers (`request.headers.get("cookie")`) or parse cookies exactly as you would in any web app. The `response.headers` object on `requestInfo` is mutable, which lets middleware append headers or set cookies that the final response will include.

`ctx` is the request-scoped object that RedwoodSDK passes to middleware, routes, React Server Components, and Server Actions. Populate it inside middleware so every downstream handler sees the same session data. Place middleware near the top of `defineApp` so it runs before any route handlers. The snippet below uses the `sessionStore` defined earlier in this guide.

Per-route interruptors work the same way. When you pass an array to `route()`, every function before the final handler is treated as a route-scoped middleware. These interruptors run after the global middleware, can mutate `ctx`, can read or write headers, and can short-circuit a request by returning or throwing a `Response`.

```tsx title="src/worker.tsx"
import { defineApp, ErrorResponse } from "rwsdk/worker";
import { route } from "rwsdk/router";

export default defineApp([
  async function sessionMiddleware({ request, ctx }) {
    const session = await sessionStore.load(request);
    ctx.session = session ?? { userId: null };
  },
  async function requireUser({ ctx }) {
    if (!ctx.session?.userId) {
      throw new ErrorResponse(401, "Unauthorized");
    }
  },
  route("/dashboard", ({ ctx }) => {
    return new Response(`User: ${ctx.session.userId}`);
  }),
]);
```

When a middleware throws an `ErrorResponse`, RedwoodSDK stops the pipeline and returns the contained status code and message. Throwing a `Response` has the same effect. Throwing any other error causes the worker to log the error and rethrow, which surfaces as an unhandled exception.
